<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据库可视化建模工具</title>
<style>
  body { 
    margin:0; 
    font-family: "Segoe UI", Roboto, sans-serif; 
    display:flex; 
    height:100vh; 
    background:#0d0d0d; 
    color:#e0e0e0;
  }
  #sidebar { 
    width:250px; 
    padding:15px; 
    border-right:1px solid #222; 
    background:#111; 
    box-sizing:border-box;
    display:flex;
    flex-direction:column;
  }
  #sidebar h3 { 
    margin:0 0 10px; 
    font-size:16px; 
    color:#fff;
  }
  #graphContainer { 
    flex:1; 
    position:relative; 
    background:#0d0d0d; 
    overflow:auto; 
  }
  button { 
    margin-top:10px; 
    padding:10px; 
    width:100%; 
    cursor:pointer; 
    border:none; 
    border-radius:8px;
    background:#1DA1F2; 
    color:#fff; 
    font-size:14px; 
    font-weight:bold;
    transition:background 0.2s ease;
  }
  button:hover {
    background:#0d8ddb;
  }
  textarea { 
    width:100%; 
    height:200px; 
    margin-top:15px; 
    background:#1a1a1a; 
    color:#f1f1f1; 
    border:1px solid #333;
    border-radius:6px; 
    padding:8px; 
    font-family:monospace; 
    font-size:13px;
  }

  #contextMenu {
      position:absolute; 
      background:#1a1a1a; 
      border:1px solid #333; 
      box-shadow:0 2px 8px rgba(0,0,0,0.8); 
      display:none; 
      z-index:1000; 
      padding:5px;
      font-size:14px;
      border-radius:6px;
  }
  #contextMenu div {
      padding:6px 10px;
      cursor:pointer;
      color:#ddd;
  }
  #contextMenu div:hover {
      background:linear-gradient(90deg, #1DA1F2, #ff0050);
      color:#fff;
  }
</style>
</head>
<body>

<div id="sidebar">
	<img style="height: 50px; width: 50px;" src="https://youke1.picui.cn/s1/2025/09/12/68c3c9a3c347f.jpg" alt="作者">
  <h3>大帅比</h3>
  <p>由于没有大模型的tokens,所以请自行复制生成的文字描述，然后丢给大模型生成sql语句</p>
  <button id="addTableBtn">➕ 添加表格</button>
  <button id="generateBtn">📝 生成文字描述</button>
  <button id="reverseBtn">🔄 逆向生成表格</button>
  <textarea id="output"></textarea>
</div>

<div id="graphContainer"></div>
<div id="contextMenu"></div>

<script src="https://cdn.jsdelivr.net/npm/mxgraph@4.2.2/javascript/mxClient.min.js"></script>
<script>
let graph;
let tableCount = 0;
const contextMenu = document.getElementById('contextMenu');

function initGraph() {
  if (!mxClient.isBrowserSupported()) { alert('浏览器不支持 mxGraph'); return; }
  const graphContainer = document.getElementById('graphContainer');
  graph = new mxGraph(graphContainer);

  graph.setConnectable(true);
  graph.setEdgeLabelsMovable(false);
  graph.setAllowDanglingEdges(false);
  graph.setPortsEnabled(true);
  graph.setHtmlLabels(true);
  graph.setCellsMovable(true);
  graph.setCellsResizable(false);
  graph.setCellsEditable(true);
  graph.setPanning(true);
  graph.panningHandler.useLeftButtonForPanning = true;
  graph.connectionHandler.isConnectableCell = function(cell) {
    return true; // 任何有 connectable=true 的 cell 都允许
  };

  const style = graph.getStylesheet().getDefaultVertexStyle();
  style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_RECTANGLE;
  style[mxConstants.STYLE_FILLCOLOR] = '#1a1a1a';
  style[mxConstants.STYLE_STROKECOLOR] = '#1DA1F2';
  style[mxConstants.STYLE_ROUNDED] = true;
  style[mxConstants.STYLE_FONTCOLOR] = '#f1f1f1';
  style[mxConstants.STYLE_FONTSIZE] = 12;

  const edgeStyle = graph.getStylesheet().getDefaultEdgeStyle();
  edgeStyle[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW;
  edgeStyle[mxConstants.STYLE_STROKECOLOR] = '#ff0050';
  edgeStyle[mxConstants.STYLE_ENDARROW] = mxConstants.ARROW_CLASSIC;

  graph.connectionHandler.isValidSource = function(cell) {
      return cell.parent && cell.parent.fields;
  };
  graph.connectionHandler.isValidTarget = function(cell) {
      return cell.parent && cell.parent.fields;
  };
  graph.connectionHandler.createEdgeState = function(me) {
      const edge = graph.createEdge(null, null, '', null, null, 'strokeColor=#1DA1F2;endArrow=classic;');
      return graph.view.createState(edge);
  };

  graph.dblClick = function(evt, cell) {
    if(cell) graph.startEditingAtCell(cell);
  };

  graphContainer.addEventListener('contextmenu', function(evt) {
      evt.preventDefault();
      evt.stopImmediatePropagation();
      const rect = graphContainer.getBoundingClientRect();
      const x = evt.clientX - rect.left;
      const y = evt.clientY - rect.top;
      const cell = graph.getCellAt(x, y);
      if (cell) {
          showContextMenu(cell, evt);
      } else {
          hideContextMenu();
      }
      return false;
  }, true);

  document.addEventListener('click', hideContextMenu);

  function showContextMenu(cell, evt) {
      contextMenu.innerHTML = '';
      if(cell.fields){ // 表格
          const addField = document.createElement('div');
          addField.innerText = '➕ 添加字段';
          addField.onclick = () => { 
              addFieldToTable(cell, '字段'+(cell.fields.length+1), 'VARCHAR(50)', '注释'); 
              hideContextMenu();
          };
          contextMenu.appendChild(addField);

          const delTable = document.createElement('div');
          delTable.innerText = '🗑️ 删除表格';
          delTable.onclick = () => { 
              graph.removeCells([cell]); 
              hideContextMenu(); 
          };
          contextMenu.appendChild(delTable);
      } else { // 字段
          const delField = document.createElement('div');
          delField.innerText = '🗑️ 删除字段';
          delField.onclick = () => {
              const parent = cell.parent;
              if(parent && parent.fields){
                  parent.fields = parent.fields.filter(f => f!==cell);
                  const newHeight = 40 + parent.fields.length *25;
                  const geo = parent.geometry;
                  geo.height = newHeight;
                  graph.getModel().setGeometry(parent, geo);
              }
              graph.removeCells([cell]);
              hideContextMenu();
          };
          contextMenu.appendChild(delField);
		  
		  const addFK = document.createElement('div');
		  addFK.innerText = '🔗 添加外键';
		  addFK.onclick = () => {
		      const refTableName = prompt("引用表名:");
		      const refFieldName = prompt("引用字段名:");
		      if(refTableName && refFieldName){
		          addForeignKey(cell, refTableName, refFieldName);
		      }
		      hideContextMenu();
		  };
		  contextMenu.appendChild(addFK);

		  
      }
      contextMenu.style.left = evt.clientX + 'px';
      contextMenu.style.top = evt.clientY + 'px';
      contextMenu.style.display = 'block';
  }
}
function addForeignKey(fieldVertex, refTable, refField) {
    fieldVertex.fk = { refTable, refField };
}


function hideContextMenu(){
    contextMenu.style.display='none';
}

function createTableVertex(tableName="Table_"+(++tableCount), x=50, y=50) {
  graph.getModel().beginUpdate();
  try {
    const tableVertex = graph.insertVertex(graph.getDefaultParent(), null, tableName,
      x, y, 200, 40,
       'shape=rectangle;whiteSpace=wrap;html=1;fillColor=#111;fontColor=#1DA1F2;fontStyle=1;verticalAlign=top;labelPosition=center;spacingTop=8;');
    tableVertex.fields = [];
    return tableVertex;
  } finally {
    graph.getModel().endUpdate();
  }
}

function addFieldToTable(tableVertex, fieldName, type, comment) {
  graph.getModel().beginUpdate();
  try {
    const yOffset = 40 + tableVertex.fields.length * 25;
    const fieldVertex = graph.insertVertex(tableVertex, null,
      `${fieldName} ${type} ${comment}`, 0, yOffset, 200, 25,
      'shape=rectangle;whiteSpace=wrap;html=1;fillColor=#1a1a1a;strokeColor=#333;fontColor=#ddd;');
   
   // 👉 新增：初始化外键属性
       fieldVertex.fk = null; 
	tableVertex.fields.push(fieldVertex);
    const newHeight = 40 + tableVertex.fields.length * 25;
    const geo = tableVertex.geometry;
    geo.height = newHeight;
	
	const port = graph.insertVertex(fieldVertex, null, '',
	  1, 0.5, 10, 10,
	  'shape=ellipse;perimeter=ellipsePerimeter;fillColor=#1DA1F2;strokeColor=#fff;resizable=0;movable=0;');
	port.geometry.offset = new mxPoint(-5, -5);
	port.geometry.offset = new mxPoint(-5, -5);
	port.geometry.relative = true;
	port.connectable = true;
	graph.getModel().setStyle(port, port.style + ';connectable=1;');


	
    graph.getModel().setGeometry(tableVertex, geo);
  } finally {
    graph.getModel().endUpdate();
  }
}

function generateText() {
    let result = '';
    const vertices = graph.getChildVertices(graph.getDefaultParent());

    vertices.forEach(table => {
        if (table.parent !== graph.getDefaultParent()) return;

        const tableName = table.value.trim();
        result += `CREATE TABLE ${tableName} (\n`;

        const fields = graph.getChildVertices(table);
        fields.forEach(f => {
            const line = f.value.trim();
            if (line) result += `  ${line},\n`;
        });

        // 🔥 自动生成外键：扫描所有 edge
        const edges = graph.getChildEdges(graph.getDefaultParent());
        edges.forEach(edge => {
            const fromField = edge.source;
            const toField = edge.target;

            if (!fromField || !toField) return;

            // 判断源字段属于当前表
            if (fromField.parent === table) {
                const fromFieldName = fromField.value.split(' ')[0];
                const toTableName = toField.parent.value.trim();
                const toFieldName = toField.value.split(' ')[0];
                result += `  FOREIGN KEY (${fromFieldName}) REFERENCES ${toTableName}(${toFieldName}),\n`;
            }
        });

        result = result.replace(/,\n$/, '\n') + ');\n\n';
    });

    document.getElementById('output').value = result;
}

function reverseGenerate() {
    const sql = document.getElementById('output').value;
    const tableRegex = /CREATE\s+TABLE\s+(\w+)\s*\(([\s\S]*?)\);/gi;
    let match;
    let pos = 50;

    const tablesMap = {}; // 表名 -> tableVertex

    // 第一步：创建表和字段
    while ((match = tableRegex.exec(sql)) !== null) {
        const tableName = match[1];
        const fieldsBlock = match[2].trim();
        const tableVertex = createTableVertex(tableName, pos, pos);
        tablesMap[tableName] = tableVertex;
        pos += 60;

        const lines = fieldsBlock.split(/\n/).map(l => l.trim()).filter(l => l && !l.startsWith('FOREIGN KEY'));
        lines.forEach(line => {
            const parts = line.replace(/,$/, '').split(/\s+/);
            const fname = parts[0] || "字段";
            const ftype = parts[1] || "";
            const fcomment = parts.slice(2).join(" ") || "";
            addFieldToTable(tableVertex, fname, ftype, fcomment);
        });
    }

    // 第二步：生成外键箭头
    tableRegex.lastIndex = 0; // 重置正则
    while ((match = tableRegex.exec(sql)) !== null) {
        const tableName = match[1];
        const fieldsBlock = match[2].trim();
        const tableVertex = tablesMap[tableName];

        const fkRegex = /FOREIGN\s+KEY\s*\((\w+)\)\s+REFERENCES\s+(\w+)\s*\((\w+)\)/gi;
        let fkMatch;
        while ((fkMatch = fkRegex.exec(fieldsBlock)) !== null) {
            const fromFieldName = fkMatch[1];
            const refTableName = fkMatch[2];
            const refFieldName = fkMatch[3];

            const fromField = tableVertex.fields.find(f => f.value.startsWith(fromFieldName));
            if (!fromField) continue;

            const refTable = tablesMap[refTableName];
            if (!refTable) continue;

            const toField = refTable.fields.find(f => f.value.startsWith(refFieldName));
            if (!toField) continue;

            const fromPort = fromField.children[0];
            const toPort = toField.children[0];

            graph.insertEdge(graph.getDefaultParent(), null, '',
                fromPort, toPort,
                'strokeColor=#ff0050;endArrow=classic;');
        }
    }
}




document.getElementById('addTableBtn').addEventListener('click', ()=>createTableVertex());
document.getElementById('generateBtn').addEventListener('click', generateText);
document.getElementById('reverseBtn').addEventListener('click', reverseGenerate);

window.onload = initGraph;
</script>
</body>
</html>
